/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package axil.compiler.ast;

import axil.api.AxilObject;
import axil.api.extend.Function;
import axil.compiler.Compilation;
import axil.compiler.Scope;
import axil.compiler.api.Emitter;
import axil.compiler.api.ScopeInjector;
import axil.compiler.api.Verifier;
import axil.compiler.lexer.Identifier;
import axil.compiler.lexer.Literal;
import axil.compiler.lexer.Literal.Species;
import axil.definition.Evaluation;
import axil.definition.FunctionDefinition;
import axil.framework.error.CompilationException;
import axil.framework.localization.Message;
import axil.stdlib.access.function.Use;
import axil.stdlib.collection.type.Range;
import axil.vm.opcode.OpCallEager;
import axil.vm.opcode.OpCallLazy;
import axil.vm.opcode.Opcode;

import java.util.ArrayList;
import java.util.List;

import static axil.framework.Functions.debug;
import static axil.framework.Functions.nv;


/**
 * An AST node defining a function call.
 */
public class FunctionCallNode extends AbstractNode {
	private Identifier name;
	private ArrayList<Node> args;
	private FunctionDefinition func;


	public FunctionCallNode(Identifier func) {
		this.name = func;
		this.args = new ArrayList<Node>(4);
	}


	public void arg(Node arg) {
		args.add(arg);
	}


	public Node arg(int index) {
		return args.get(index);
	}


	public String function() {
		return name.identity();
	}


	/**
	 * Provide a brief description of what this node is. This should include the
	 * kind of thing it is, not the contents of the thing.
	 */
	public String what() {
		return "function call";
	}


	/**
	 * Get the identifier defined by this node. If this node does not define an
	 * identifier, then an exception is thrown.
	 */
	public String identity() {
		return name.identity();
	}


	/**
	 * Dump the content of this node to the diagnostic device. This is strictly
	 * for internal diagnostics, not for application use.
	 */
	public void dump(int indent) {
		debug(indent, "CALL " + function());
		indent ++;
		int i = 0;
		for (Node n : args) {
			debug(indent, "ARG[" + i + ']');
			n.dump(indent + 1);
			i++;
		}
	}


	/**
	 * Compile this node, verifying correctness and performing any optimizations
	 * possible. The compilation context and scope given are never null but may
	 * not contain the desired functions and identifiers.
	 */
	public Node compile(Compilation context, Scope scope) {
		func = context.function(name.identity());
		if (func == null) {
			throw problem(this, "no-such-function",
			              nv("function", name.identity()));
		}
		if (! func.accepts(args)) {
			throw problem(this, "invalid-arg-count",
			              nv("function", name.toString()),
			              nv("synopsis", func.synopsis()));
		}
		Function f = null;

		/*
		 * All function calls introduce a new scope, since the call stack at
		 * runtime will increase in depth by one. However, only some functions
		 * introduce a new identifier into scope.
		 */
		scope = new Scope(scope);
		if (func.code().isNative()) {
			f = func.function();
			if (f instanceof ScopeInjector) {
				/*
				 * In this case, the function is special in that it places new
				 * identifiers in scope at the time of execution. For example, the
				 * let(), func() and with() functions all put new values into scope.
				 * Therefore, we need to create a new scope that exists for this
				 * point downward.
				 */
				inject(scope, (ScopeInjector)f);
			}
		}

		/**
		 * Now we pass over the arguments to this function, compiling each of
		 * these as well. The compilation process may completely replace the
		 * node with a different node.
		 */
		final int size = args.size();
		for (int i = 0; i < size; i++) {
			args.set(i, args.get(i).compile(context, scope));
		}
		uses(context, f);

		if ((f != null) && (f instanceof Verifier)) {
			/*
			 * If the function has special verification abilities, we activate
			 * these now. If a message is returned, it is an indication of a
			 * problem.
			 */
			Message msg = ((Verifier)f).verify(args);
			if (msg != null) {
				throw new CompilationException(this, msg);
			}
		}
		return this;
	}


	/**
	 * The mechanism by which dynamic access to object members can be statically
	 * declared. See the documentation in the Use function class for details of
	 * why this is useful and how it works. An obscure and silly feature, but
	 * one the masses have requested.
	 */
	private void uses(Compilation context, Function f) {
		if ((f != null) && (f instanceof Use)) {
			final int size = args.size();
			String root = root(args.get(0));
			context.references(root);
			for (int i = 1; i < size; i++) {
				expand(context, root, args.get(i));
			}
		}
	}


	private String root(Node node) {
		if (node instanceof IdentifierNode) {
			return node.identity();
		}
		if (node instanceof LiteralNode) {
			return text(node);
		}
		throw problem("malformed-use-root", nv("kind", node.what()));
	}


	private void expand(Compilation context, String root, Node node) {
		if (node instanceof IdentifierNode) {
			context.references(concat(root, node.identity()));
			return;
		}
		if (node instanceof LiteralNode) {
			context.references(concat(root, text(node)));
			return;
		}
		if (node instanceof FunctionCallNode) {
			FunctionCallNode fc = (FunctionCallNode)node;
			if (fc.identity().equals("list")) {
				final List<Node> a = fc.args;
				if (a.size() < 2) {
					throw problem("use-list-too-small", nv("size", a.size()));
				}
				root = concat(root, text(a.get(0)));
				context.references(root);
				for (int i = 1; i < a.size(); i ++) {
					context.references(concat(root, text(a.get(i))));
				}
				return;
			}
		}
		throw problem("malformed-use-argument", nv("kind", node.what()));
	}


	private String text(Node node) {
		if (node instanceof LiteralNode) {
			Literal lit = ((LiteralNode)node).value();
			if (! lit.extended()) {
				if (lit.species() == Species.STRING) {
					return lit.text();
				}
			}
		}
		throw problem("malformed-use-argument", nv("kind", node.what()));
	}


	private String concat(String root, String field) {
		return root + '.' + field;
	}


	private void inject(Scope scope, ScopeInjector injector) {
		Range ids = injector.injects(args.size());
		if (ids == null) {
			throw problem(this, "invalid-arg-count",
			              nv("function", name.toString()),
			              nv("synopsis", func.synopsis()));
		}
		for (int i = ids.min(); i <= ids.max(); i++) {
			Node arg = args.get(i);
			if (injector.indirect()) {
				if (! (arg instanceof FunctionCallNode)) {
					throw problem(this, "expecting-var-decl",
					              nv("function", name.toString()), nv("arg", i + 1));
				}
				FunctionCallNode fc = (FunctionCallNode)arg;
				if (fc.args.size() < 1) {
					throw problem(this, "expecting-var-decl",
					              nv("function", name.toString()), nv("arg", i + 1));
				}
				arg = fc.args.get(0);
			}
			if (! (arg instanceof IdentifierNode)) {
				throw problem(this, "expecting-ident",
				              nv("function", name.identity()),
				              nv("that", arg.what()),
				              nv("arg", i + 1));
			}
			IdentifierNode ident = (IdentifierNode)arg;
			if (! simple(ident)) {
				throw problem(this, "invalid-scope-var",
				              nv("function", name.toString()), nv("name", ident));
			}
			if (scope.defines(ident)) {
				throw problem(this, "duplicate-ident",
				              nv("function", name.toString()), nv("name", ident));
			}
			if (scope.parent().defines(ident)) {
				throw problem(this, "hides-outer-scope",
				              nv("function", name.toString()), nv("name", ident));
			}
			scope.put(ident);
		}
	}


	/**
	 * Tell if the identifier given is simple or compound. We lean on the lexer
	 * to have straightened out any syntactic issues before we get here, so this
	 * should be sufficient. Note that we cannot use the "kind" indicator in the
	 * identifier, since it is not yet known.
	 */
	private boolean simple(IdentifierNode ident) {
		return ident.identity().indexOf('.') < 0;
	}


	private AxilObject[] arguments() {
		final int size = args.size();
		AxilObject[] objects = new AxilObject[size];
		for (int i = 0; i < size; i++) {
			objects[i] = args.get(i).emit();
		}
		return objects;
	}


	/**
	 * Emit VM instructions that represents an executable version of this AST
	 * node. The instruction returned cannot be null. An exception is thrown if
	 * any latent defects in the AST are discovered.
	 */
	public Opcode emit() {
		Function f = func.function();
		if (f instanceof Emitter) {
			return ((Emitter)f).emit(this, args);
		}
		if (func.code().evaluation() == Evaluation.lazy) {
			return new OpCallLazy(this, f, func.ids(), arguments());
		}
		return new OpCallEager(this, f, func.ids(), arguments());
	}
}
